package island

import (
	"context"
	"encoding/json"
	"fmt"
	"github.com/Unixeno/islands/conf"
	"github.com/Unixeno/islands/logging"
	"github.com/containerd/go-runc"
	"github.com/opencontainers/runtime-spec/specs-go"
	"golang.org/x/sys/unix"
	"io/ioutil"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"
)

const (
	resolveConfig = ``
	hostsConfig   = `127.0.0.1	localhost
# The following lines are desirable for IPv6 capable hosts
::1     ip6-localhost ip6-loopback
fe00::0 ip6-localnet
ff00::0 ip6-mcastprefix
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
`
)

// 创建 init 层包括写入 hosts hostname resolv.conf
func initLayer(id string, c *initConfig) error {
	containerPath := filepath.Join(containerRootDir, id)
	if err := os.MkdirAll(containerPath, 0755); err != nil {
		return err
	}

	// 写入 hostname resolv.conf hosts
	if err := ioutil.WriteFile(containerPath+"/hostname", []byte(c.Hostname), 0644); err != nil {
		return err
	}

	fileContent := ""

	for _, d := range c.DNS {
		fileContent += strings.Join([]string{"nameserver", d, "\n"}, " ")
	}

	if err := ioutil.WriteFile(containerPath+"/resolv.conf", []byte(resolveConfig+fileContent), 0644); err != nil {
		return err
	}

	fileContent = ""

	for _, entry := range c.Hosts {
		fileContent += strings.Join([]string{entry.Address, entry.Host, "\n"}, " ")
	}

	if err := ioutil.WriteFile(containerPath+"/hosts", []byte(hostsConfig+fileContent), 0644); err != nil {
		return err
	}

	// 创建 init layer 挂载点
	initEtcPath := filepath.Join(containerPath, "init", "etc")
	if err := os.MkdirAll(initEtcPath, 0755); err != nil {
		return err
	}

	paths := []string{
		filepath.Join(containerPath, "init", "etc", "hosts"),
		filepath.Join(containerPath, "init", "etc", "resolv.conf"),
		filepath.Join(containerPath, "init", "etc", "hostname"),
	}

	for _, p := range paths {
		if err := ioutil.WriteFile(p, []byte{}, 0644); err != nil {
			return err
		}
	}

	return nil
}

func baseSpec(config *baseSpecInfo) *specs.Spec {
	spec := &specs.Spec{
		Version: specs.Version,
		Root: &specs.Root{
			Path:     config.rootfs,
			Readonly: false,
		},
		Process: &specs.Process{
			User: specs.User{
				UID:            0,
				GID:            0,
				AdditionalGids: nil,
				Username:       config.username,
			},
			Args:            config.args,
			Env:             config.env,
			Cwd:             config.workingDir,
			NoNewPrivileges: true,
			Capabilities: &specs.LinuxCapabilities{
				Bounding: []string{
					"CAP_CHOWN",
					"CAP_DAC_OVERRIDE",
					"CAP_FSETID",
					"CAP_FOWNER",
					"CAP_MKNOD",
					"CAP_NET_RAW",
					"CAP_SETGID",
					"CAP_SETUID",
					"CAP_SETFCAP",
					"CAP_SETPCAP",
					"CAP_NET_BIND_SERVICE",
					"CAP_SYS_CHROOT",
					"CAP_KILL",
					"CAP_AUDIT_WRITE",
				},
				Permitted: []string{
					"CAP_CHOWN",
					"CAP_DAC_OVERRIDE",
					"CAP_FSETID",
					"CAP_FOWNER",
					"CAP_MKNOD",
					"CAP_NET_RAW",
					"CAP_SETGID",
					"CAP_SETUID",
					"CAP_SETFCAP",
					"CAP_SETPCAP",
					"CAP_NET_BIND_SERVICE",
					"CAP_SYS_CHROOT",
					"CAP_KILL",
					"CAP_AUDIT_WRITE",
				},
				Inheritable: []string{
					"CAP_CHOWN",
					"CAP_DAC_OVERRIDE",
					"CAP_FSETID",
					"CAP_FOWNER",
					"CAP_MKNOD",
					"CAP_NET_RAW",
					"CAP_SETGID",
					"CAP_SETUID",
					"CAP_SETFCAP",
					"CAP_SETPCAP",
					"CAP_NET_BIND_SERVICE",
					"CAP_SYS_CHROOT",
					"CAP_KILL",
					"CAP_AUDIT_WRITE",
				},
				Ambient: []string{
					"CAP_CHOWN",
					"CAP_DAC_OVERRIDE",
					"CAP_FSETID",
					"CAP_FOWNER",
					"CAP_MKNOD",
					"CAP_NET_RAW",
					"CAP_SETGID",
					"CAP_SETUID",
					"CAP_SETFCAP",
					"CAP_SETPCAP",
					"CAP_NET_BIND_SERVICE",
					"CAP_SYS_CHROOT",
					"CAP_KILL",
					"CAP_AUDIT_WRITE",
				},
				Effective: []string{
					"CAP_CHOWN",
					"CAP_DAC_OVERRIDE",
					"CAP_FSETID",
					"CAP_FOWNER",
					"CAP_MKNOD",
					"CAP_NET_RAW",
					"CAP_SETGID",
					"CAP_SETUID",
					"CAP_SETFCAP",
					"CAP_SETPCAP",
					"CAP_NET_BIND_SERVICE",
					"CAP_SYS_CHROOT",
					"CAP_KILL",
					"CAP_AUDIT_WRITE",
				},
			},
			Rlimits: []specs.POSIXRlimit{},
		},
		Hostname: config.hostname,
		Mounts: []specs.Mount{
			{
				Destination: "/proc",
				Type:        "proc",
				Source:      "proc",
				Options:     nil,
			},
			{
				Destination: "/dev",
				Type:        "tmpfs",
				Source:      "tmpfs",
				Options:     []string{"nosuid", "strictatime", "mode=755", "size=65536k"},
			},
			{
				Destination: "/dev/shm",
				Type:        "tmpfs",
				Source:      "shm",
				Options:     []string{"nosuid", "noexec", "nodev", "mode=1777", "size=65536k"},
			},
			{
				Destination: "/dev/mqueue",
				Type:        "mqueue",
				Source:      "mqueue",
				Options:     []string{"nosuid", "noexec", "nodev"},
			},
			{
				Destination: "/dev/pts",
				Type:        "devpts",
				Source:      "devpts",
				Options: []string{"nosuid",
					"noexec",
					"newinstance",
					"ptmxmode=0666",
					"mode=0620",
					"gid=5",
				},
			},
			{
				Destination: "/sys",
				Type:        "sysfs",
				Source:      "sysfs",
				Options:     []string{"nosuid", "noexec", "nodev", "ro"},
			},
		},
		Linux: &specs.Linux{
			// 暂时不需要
			MaskedPaths:   []string{},
			ReadonlyPaths: []string{},
			Resources:     &specs.LinuxResources{},
			Namespaces:    nil,
		},
	}
	return spec
}

func List() []ContainerInfo {
	list := make([]ContainerInfo, 0)
	db.Range(func(k, v interface{}) bool {
		item := v.(*dbItem)
		container := item.container
		command := strings.Join(container.RuntimeSetting.Args, " ")
		info := ContainerInfo{
			Id:      container.ID,
			Name:    container.Name,
			ImageId: container.ImageSetting.ImageID,
			Command: command,
			Created: container.CreateTime,
			Status:  container.State.String(),
		}
		list = append(list, info)
		return true
	})
	return list
}

func PathExist(path string) bool {
	if _, err := os.Stat(path); os.IsNotExist(err) {
		return false
	}

	return true
}

func envMapToSlice(m map[string]string) []string {
	env := make([]string, len(m))
	idx := 0
	for k, v := range m {
		env[idx] = k + "=" + v
		idx += 1
	}
	return env
}

func envSliceToMap(s []string) map[string]string {
	env := map[string]string{}
	for _, str := range s {
		t := strings.SplitN(str, "=", 2)
		if t == nil || t[1] == "" {
			continue
		}
		env[t[0]] = t[1]
	}
	return env
}

func newBaseContainer() *base {
	c := base{}
	c.State = NewState()
	return &c
}

func recoverContainerFromFile(path string) *base {
	data, err := ioutil.ReadFile(path)
	if err != nil {
		return nil
	}
	container := newBaseContainer()
	if err = json.Unmarshal(data, container); err != nil {
		return nil
	}

	if container.IsRunning() || container.IsPaused() || container.IsRestarting() {
		container.SetStopped()
	}

	return container
}

func appendInitMount(spec *specs.Spec, config *initConfig) {
	m := map[string]string{
		config.HostsMountPath:    "/etc/hosts",
		config.HostnameMountPath: "/etc/hostname",
		config.DNSMountPath:      "/etc/resolv.conf",
	}

	for k, v := range m {
		spec.Mounts = append(spec.Mounts, specs.Mount{
			Destination: v,
			Type:        "none",
			Source:      k,
			Options: []string{
				// crun 好像不支持 bind
				"rbind",
			},
		})
	}

}

func addTTY(spec *specs.Spec, container *base) {
	if !container.RuntimeSetting.TTY {
		return
	}
	spec.Process.Terminal = true
}

const setChildSubReaper = 36

func setSubReaper(i int) error {
	return unix.Prctl(setChildSubReaper, uintptr(i), 0, 0, 0)
}

func signalHandle(ctx context.Context, signals chan os.Signal) error {
	logger.Debug("start signal loop")

	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case s := <-signals:
			switch s {
			case unix.SIGCHLD:
				// todo: 简单保证 go-runc 的 wait runc 在这里之前 正常操作应该是加锁
				time.Sleep(time.Millisecond)
				if err := unixReap(); err != nil {
					logger.Error("cannot call unixReap")
				}
			}
		}
	}
}

func unixReap() error {
	exits, err := Reap(false)
	processInfo.ProcessLock.Lock()
	defer processInfo.ProcessLock.Unlock()
	for _, e := range exits {
		if _, ok := processInfo.ProcessMap[e.Pid]; !ok {
			return nil
		}
		processInfo.ProcessMap[e.Pid].SetExitCode(e.Status)
		close(processInfo.ProcessMap[e.Pid].ExitChan)
	}
	return err
}

func Reap(wait bool) (exits []Exit, err error) {
	var (
		waitState     unix.WaitStatus
		resourceUsage unix.Rusage
	)
	flag := unix.WNOHANG
	if wait {
		flag = 0
	}
	for {
		pid, err := unix.Wait4(-1, &waitState, flag, &resourceUsage)
		if err != nil {
			if err == unix.ECHILD {
				return exits, nil
			}
			return exits, err
		}
		if pid <= 0 {
			return exits, nil
		}
		exits = append(exits, Exit{
			Pid:    pid,
			Status: exitStatus(waitState),
		})
	}
}

const exitSignalOffset = 128

func exitStatus(status unix.WaitStatus) int {
	if status.Signaled() {
		return exitSignalOffset + int(status.Signal())
	}
	return status.ExitStatus()
}

func checkStatus(id string, target ...string) bool {
	item, ok := db.getItem(id)
	if !ok {
		return false
	}
	source := item.container.State.String()
	for _, s := range target {
		if s == source {
			return true
		}
	}
	return false
}

func runcErrHandle(IO runc.IO, id string) error {
	buf := make([]byte, 1024)
	_, err := IO.Stderr().Read(buf)
	if err != nil {
		_ = IO.Close()
		return err
	}
	_ = IO.Close()
	return fmt.Errorf("%v", strings.Split(string(buf), "\n")[1])
}

func saveCheck(container *base) {
	// 检查 runtime Setting
	if _, ok := container.RuntimeSetting.Env["PATH"]; !ok {
		container.RuntimeSetting.Env["PATH"] = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
	}

	if container.RuntimeSetting.User == "" {
		container.RuntimeSetting.User = "root"
	}

	if container.RuntimeSetting.WorkingDir == "" {
		container.RuntimeSetting.WorkingDir = "/"
	}

	// 添加 hosts
	found := false
	for _, entry := range container.InitSetting.Hosts {
		if entry.Address == "127.0.0.1" && entry.Host == container.InitSetting.Hostname {
			found = true
			break
		}
	}
	if !found {
		container.InitSetting.Hosts = append(container.InitSetting.Hosts, &HostEntry{
			Host:    container.InitSetting.Hostname,
			Address: "127.0.0.1",
		})
	}
}

func containerArgs(container *base) []string {
	var args []string
	if container.RuntimeSetting.Entrypoint == nil || len(container.RuntimeSetting.Entrypoint) == 0 {
		args = container.RuntimeSetting.Cmd
		if args == nil || len(args) == 0 {
			// 默认 args
			args = append(args, "bash")
		}
	} else {
		args = append(args, container.RuntimeSetting.Entrypoint[0])
		if container.RuntimeSetting.Cmd != nil && len(container.RuntimeSetting.Cmd) > 0 {
			args = append(args, container.RuntimeSetting.Cmd[:]...)
		}
	}

	return args
}

func appendVolume(spec *specs.Spec, container *base) error {
	if len(container.RuntimeSetting.Volumes) != 0 {
		if err := appendVolumeMount(spec, container.RuntimeSetting.Volumes, container.ID); err != nil {
			logger.WithError(err).Error("failed to add volume in save")
			return err
		}
	}

	return nil
}

func saveConfigJson(spec *specs.Spec, container *base) error {
	var data []byte
	var err error
	if logger.Level == logging.DebugLevel {
		data, err = json.MarshalIndent(spec, "", "\t")
	} else {
		data, err = json.MarshalIndent(spec, "", "")
	}

	if err != nil {
		logger.WithError(err).Error("failed to generate container spec data")
		return err
	}
	if err = ioutil.WriteFile(filepath.Join(filepath.Dir(container.Rootfs), "config.json"), data, 0600); err != nil {
		logger.WithError(err).Error("failed to write config.json for runc")
		return err
	}
	return nil
}

func defaultDns() []string {
	dns := strings.Split(conf.GetString("DefaultDNS"), ",")
	if len(dns) == 0 {
		dns = append(dns, "8.8.8.8", "8.8.4.4")
	} else {
		// 需要 trim
		for i := range dns {
			dns[i] = strings.TrimSpace(dns[i])
		}
	}
	return dns
}

// 检查 runtime tool 仅支持 runc crun
func validRuntimeTool(path string) error {
	output, err := exec.Command(path, "--v").Output()
	if err != nil {
		return err
	}
	toolName = strings.Split(string(output), " ")[0]

	if toolName != "crun" && toolName != "runc" {
		return fmt.Errorf("don't support this runtime tool")
	}
	logger.WithField("RuntimeTool", toolName).Info()
	return nil
}

func baseNamespace() map[string]specs.LinuxNamespace {
	return map[string]specs.LinuxNamespace{
		// 默认不使用 user cgroup
		"pid": {
			Type: "pid",
		},
		"network": {
			Type: "network",
		},
		"ipc": {
			Type: "ipc",
		},
		"uts": {
			Type: "uts",
		},
		"mount": {
			Type: "mount",
		},
	}
}

func addSpecNamespace(spec *specs.Spec, container *base) {
	spec.Linux.Namespaces = make([]specs.LinuxNamespace, 0, len(container.RuntimeSetting.Namespace))
	for _, v := range container.RuntimeSetting.Namespace {
		spec.Linux.Namespaces = append(spec.Linux.Namespaces, v)
	}
}
